Skip to content

refactor(sync-figma): convert SVG with SVGR + svgo instead of regex - #715

Merged
MaxLee-dev merged 9 commits into
mainfrom
feat/icons-svgr-pipeline
Sep 3, 2026
Merged

refactor(sync-figma): convert SVG with SVGR + svgo instead of regex#715
MaxLee-dev merged 9 commits into
mainfrom
feat/icons-svgr-pipeline

Conversation

@MaxLee-dev

@MaxLee-dev MaxLee-dev commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Related Issues

Description of Changes

왜 바꾸나

Figma SVG를 React 컴포넌트로 바꾸는 일을 transforms.ts의 정규식 세 함수가 맡고 있었습니다. 문자열 치환이라 두 군데서 샙니다.

  • remakeMaskStylestyle 속성에 선언이 둘 이상이면(mask-type:alpha;opacity:0.5) 앞의 하나만 객체로 바꾸고 나머지는 값 안에 문자열로 남깁니다.
  • makeFlexibleColorIconfill="none"<svg> 루트와 자식 구분 없이 지웁니다. Figma는 루트의 fill="none"으로 stroke 도형의 속을 비워 두는데, 이 규칙이 그 의도를 지웁니다.

무엇을 바꿨나

변환을 @svgr/core(+ plugin-svgo, plugin-jsx)에 넘겼습니다. 정규식이 하던 width/height 제거, 속성 camelCase, style 객체화가 전부 라이브러리 기본기로 덮입니다. 경로 데이터 최적화는 덤입니다.

transforms.ts와 문자열 템플릿 icon-component.ts를 지우고 svgr.ts 하나로 합쳤습니다. 컴포넌트 껍데기는 SVGR custom template이 만들고, 거기서 <svg> JSX 요소를 IconBase로 바꿔 끼웁니다.

커스텀 플러그인

blackFollowsCurrentColor는 mono 아이콘에서 fill/stroke의 리터럴 검정(#000, #000000, black)을 currentColor로 바꿉니다. IconBasecolor를 내려 주므로 이제 fill과 stroke가 함께 그 색을 따라갑니다. color 아이콘은 Figma 팔레트를 그대로 둬야 해서 이 플러그인을 태우지 않습니다.

Figma가 항상 붙이는 루트 fill="none"은 건드리지 않습니다. 남겨 두면 stroke만 있는 도형이 그 값을 상속해 속이 빈 채로 남습니다. 지우는 쪽을 먼저 시도했다가 stroke 도형이 꽉 찬 덩어리가 되어 되돌렸습니다(39f4729).

검증

  • pnpm --filter @repo/sync-figma typecheck 통과
  • mono·stroke·마스크가 섞인 SVG를 넣어 출력 JSX를 확인했습니다 — prefixIds 적용, 루트 fill="none" 유지, 검정 fill/strokecurrentColor
  • Figma 원본 SVG와 새 파이프라인 결과 814개를 브라우저에서 픽셀 대조했습니다. 모양 회귀 0건입니다(정규화 절대차 최댓값 0.843%, 전부 경계선 1px 헤일로).

Checklist

  • The PR title follows the Conventional Commits convention. (e.g., feat, fix, docs, style, refactor, test, chore)
  • I have added tests for my changes.
  • I have updated the Storybook or relevant documentation.
  • I have added a changeset for this change. (e.g., for any changes that affect users, such as component prop changes or new features).
  • I have performed a self-code review.
  • I have followed the project's coding conventions and component patterns.

`node` 대신 `tsx`로 실행하도록 바꾸고 `.mjs`/`.js` 11개를 `.ts`로 옮겼다.
빌드 산출물은 만들지 않는다 — 1년에 몇 번 도는 내부 CLI라 dist를 둘 이유가 없다.

타입을 붙이는 과정에서 드러난 문제 세 가지를 함께 고쳤다.

- `TYPE`이 비었거나 오타면 `ICON_TYPES[undefined]`를 구조 분해하다 죽었다. FIGMA_TOKEN 검사 옆에 가드를 뒀다.
- `sync-icons`의 `else` 분기는 도달할 수 없는 코드였다. `TYPE`은 basic 아니면 symbol뿐이라 앞 분기에서 모두 걸린다.
- `lib`의 `hasOwnProperty` 직접 호출을 `Object.hasOwn`으로 바꿨다.

`prettier`는 `sync-icons`가 이미 import하고 있었는데 선언만 빠져 있어 dependencies에 넣었다.
Figma SVG를 React 컴포넌트로 바꾸는 일을 정규식 네 단계(`transforms.ts`)가 하고 있었다.
문자열 치환이라 선언이 둘 이상인 `style` 속성에서 깨졌고, `<svg>` 루트와 자식을 구분하지 않고
`fill="none"`을 지워 stroke만 있는 아이콘을 망가뜨렸다.
이 변환을 `@svgr/core`(+`plugin-svgo`, `plugin-jsx`)에 넘긴다.

정규식이 하던 네 가지(크기 제거·속성 camelCase·`style` 객체화·색 비우기)가 전부 라이브러리
기본기로 덮이고, 덤으로 경로 데이터가 최적화된다.

기본값에서 벗어난 설정 세 가지는 각각 주석으로 이유를 남겼다.

- `prefixIds`를 명시한다. svgo의 `cleanupIds`가 id를 `a`로 줄여 두 아이콘이 한 페이지에 있으면
  마스크가 충돌한다.
- `removeViewBox: false` + `removeDimensions` 조합으로 viewBox를 남기고 width/height만 지운다.
  크기는 `IconBase`가 정한다.
- mono 아이콘은 루트 `fill="none"`을 지운다. 남기면 자식의 `fill="black"`을 제거한 뒤 상속할 색이
  없어 아이콘이 사라진다.

커스텀 플러그인 둘을 새로 뒀다. `keepStrokedShapesHollow`는 루트 `fill="none"`이 사라지기 전에
그 의도를 도형에 직접 적고, `strokeFollowsCurrentColor`는 Figma가 박아 넣은 `stroke="black"`을
`currentColor`로 바꿔 stroke도 `color`를 따르게 한다.

`prettier` 설정은 하드코딩 대신 저장소 설정을 읽어 쓴다. import 정렬 플러그인까지 걸려서
생성된 파일이 `pnpm format` 결과와 어긋나지 않는다.
@MaxLee-dev
MaxLee-dev requested a review from noahchoii as a code owner August 31, 2026 07:08
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
vapor-ui Ready Ready Preview Sep 3, 2026 12:59am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 381f227

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 36 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: d9eed6fc-4a82-48ec-81b6-c0400ee5d4b2

📥 Commits

Reviewing files that changed from the base of the PR and between c9cbbc4 and 381f227.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (6)
  • scripts/sync-figma/commands/sync-icons.ts
  • scripts/sync-figma/package.json
  • scripts/sync-figma/src/icons/templates/icon/icon-component.ts
  • scripts/sync-figma/src/integrations/figma/lib.ts
  • scripts/sync-figma/src/integrations/figma/svgr.ts
  • scripts/sync-figma/src/integrations/figma/transforms.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

/**
* Wrap the SVG in IconBase instead of rendering a bare <svg>.
*/
const template: Config['template'] = ({ componentName, jsx }, { tpl }) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IconBase, rest props 추가를 위해 template custom

커스텀하지 않는 경우 svg 태그로 나오기 때문에 IconBase로 나오기 위해 커스텀

Comment on lines +39 to +55
* Figma paints mono icons with a literal black fill (and stroke on a few), which would ignore the
* colour IconBase hands down. Point both at currentColor; the root fill="none" Figma always emits
* stays put so stroked shapes keep inheriting `none` and stay hollow.
*/
const blackFollowsCurrentColor: SvgoPlugin = {
name: 'blackFollowsCurrentColor',
fn: () => ({
element: {
enter: (node) => {
for (const attr of ['fill', 'stroke'] as const) {
if (BLACK.test(node.attributes[attr] ?? ''))
node.attributes[attr] = 'currentColor';
}
},
},
}),
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

colorIcon이 아닌 경우 black을 currentColor로 변경해야 theme 대응이 되기 때문에 black, 000, 000000을 모두 currentColor로 대체.

}): Config => ({
typescript: true,
template,
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

순서대로 파이프라인이 진행됩니다.

  1. svgo를 통해 id 양식 통일과 불필요한 속성 제거
  2. svgr을 통해 jsx ast 생성

typescript: true,
template,
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
svgoConfig: {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svgoConfig를 넘기는 경우 svgr에 설정된 svgo 설정 모두가 대체됩니다.

그래서 기본으로 들어가는 prefixIds를 다시 작성해 주어야 한다.

plugins: [
// removeViewBox would drop the viewBox because it matches width/height;
// removeDimensions then drops width/height instead, leaving IconBase in charge of size.
{ name: 'preset-default', params: { overrides: { removeViewBox: false } } },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeViewBox는 사이즈와 viewBox가 동일하면 제거하기 때문에, viewBox는 유지하고 width, height만 제거하도록 설정.

@MaxLee-dev
MaxLee-dev marked this pull request as ready for review September 2, 2026 01:43

@noahchoii noahchoii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍

Base automatically changed from refactor/sync-figma-typescript to main September 3, 2026 00:54
@MaxLee-dev
MaxLee-dev merged commit 0f9eae4 into main Sep 3, 2026
6 of 7 checks passed
@MaxLee-dev
MaxLee-dev deleted the feat/icons-svgr-pipeline branch September 3, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants